feat(agglayer): RBAC-based access control for bridge roles#3130
Conversation
Replace the bridge's hard-coded admin / GER-injector / GER-remover account-ID storage slots with the miden-standards access-control stack (Ownable2Step + RoleBasedAccessControl + Authority). The role-gated procedures (register_faucet, store_faucet_metadata_hash, update_ger, remove_ger) now call authority::assert_authorized, which checks the note sender holds the FAUCET_ADMIN / GER_INJECTOR / GER_REMOVER role mapped to that procedure. A governance owner can grant/revoke roles and transfer ownership via Ownable2Step. - miden-standards: add RoleAssignment + RoleBasedAccessControl::with_roles to seed initial role members at construction; AccessControl::Rbac gains a `members` field. - bridge MASM: drop the three ID slots / bespoke assert_sender_is_* procs; gate the four privileged procedures via authority::assert_authorized. - bridge Rust: add BridgeRoleMember, role-symbol + procedure-root accessors, and the fixed procedure->role map; create_bridge_account now takes an owner and a Vec<BridgeRoleMember> instead of three bare account IDs. - build.rs: include the RBAC stack when computing BRIDGE_CODE_COMMITMENT. - update SPEC.md, agglayer integration tests, and bench setups. On-chain role-management notes (grant/revoke role, transfer ownership) are a planned follow-up, so role rotation is not yet exercisable on-chain. Refs #2706. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AggLayerBridge is a zero-field component; use the bare value via its Default/From impl (matching PausableManager and BurnAllowAll) instead of an empty new(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Represent initial role membership as BTreeMap<RoleSymbol, BTreeSet<AccountId>> instead of Vec<RoleAssignment>. Duplicate roles and duplicate members are now impossible by construction, so with_roles drops the two dedup passes (and the DuplicateRole / DuplicateMember error variants), and the RoleAssignment type is removed. The bridge's role grouping collapses to a single entry()-based loop. No behavior change: seeding still equals runtime grant_role (equivalence test passes). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- bridge: resolve procedure roots via the procedure_root! macro + a component code() getter, matching the PausableManager pattern (removes the ad-hoc bridge_procedure_root helper). - bridge: BridgeRoleMember variants now carry a Vec<AccountId> so all holders of a role are grouped in one entry; rbac_role_members collects via entry()+extend. - docs: link role names to their BridgeRoleMember variants; tighten the bridge component and builder doc comments; bullet-list the with_roles Errors section. - masm: drop the redundant storage-slots comment. - SPEC: switch stored-ID wording to role-membership wording and keep a single authority::assert_authorized explanation (in Administration) instead of repeating it in every note/procedure section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- bridge: move the component namespace and the component-code getter into AggLayerBridge (AggLayerBridge::COMPONENT_NAMESPACE and AggLayerBridge::code()), matching the PausableManager shape. - rbac: trim the seeded roles field doc to the essential summary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve import conflicts in lib.rs and the agglayer test files, and adapt to next's AccountId::dummy 4-arg signature (AssetCallbackFlag) in the bridge test helpers. The bridge-account test call sites keep using the create_existing_bridge_account_with_roles helper, so next's direct create_existing_bridge_account import is dropped where unused. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
partylikeits1983
left a comment
There was a problem hiding this comment.
Looks really good, I like the simplification that the RBAC provides.
There are a few minor things I think should be addressed before merging.
| RoleBasedAccessControl::with_roles(members) | ||
| .expect("initial RBAC role assignments should be valid") | ||
| .into(), |
There was a problem hiding this comment.
Nit: This will panic instead of returning an error if the user passes an empty member set (or a set with more than u32::MAX members).
Can we return an error here instead of panicking? And add tests for both error cases?
There was a problem hiding this comment.
Indeed, I overlooked it. I'll update it to return an error and add tests for it
There was a problem hiding this comment.
Hmm, this update will complicate things a bit, since we expect here to have an iterator of components: changing this into iterator of results will require some changes in quite a lot of places, so I'm not sure that it's forth it. For now I'll just move this checks to the Into<AccountComponent> implementation, but in general I agree that we should somehow return an error here instead of panic.
- bridge: replace the BridgeRoleMember enum with a BridgeRoles struct (BTreeSet per role) and a fallible BridgeRoles::new that rejects an empty holder set for any role (new AgglayerBridgeError::EmptyBridgeRole), so a bridge can't be deployed with an unfillable role. create_bridge_account takes BridgeRoles. - standards: make RoleBasedAccessControl::with_roles infallible (drop empty member sets instead of erroring/panicking; remove RbacError and the IntoIterator expect). Add reusable role_config_key / role_membership_key helpers and use them in the From impl instead of building raw words. - testing: move the shared create_existing_bridge_account_with_roles / bridge_test_owner helper into miden_agglayer::testing so the test crate and bench share one copy. - tests: add ERR_SENDER_LACKS_ROLE tests for update_ger (GER_INJECTOR) and register_faucet via CONFIG (FAUCET_ADMIN), and a BridgeRoles::new empty-role test. - SPEC: point the role-management follow-up TODO at #3046; fix a doc link. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- changelog: correct the [BREAKING] entry to reflect the BridgeRoles signature (create_bridge_account takes (seed, owner, BridgeRoles)) instead of the removed BridgeRoleMember type. - rbac: note that the u32 member-count conversion in the From impl is purely defensive / infallible. - test: add test_rbac_with_roles_drops_empty_role locking in the empty-role drop semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
create_rbac_account_with_members(owner, BTreeMap::new()) covers the empty-members case, so the separate owner-only helper is unnecessary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It was only used by the shared create_existing_bridge_account_with_roles test helper, so fold the build_existing() call into that helper (via the crate-private create_bridge_account_builder) and drop the redundant public wrapper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- bridge: drop the verbose BridgeRoles struct doc paragraph (the empty-role rejection is already documented on BridgeRoles::new). - changelog: trim the breaking-change entry per review suggestion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reconcile the RBAC access-control refactor with next's new DEREGISTER_AGG_FAUCET feature: - Convert deregister_faucet from assert_sender_is_bridge_admin (removed by this branch) to authority::assert_authorized, mapped to the FAUCET_ADMIN role (procedure_roles + a new procedure_root!/deregister_faucet_root accessor). - Update next's deregister tests to the create_existing_bridge_account_with_roles helper and ERR_SENDER_LACKS_ROLE; update the procedure-roles mapping test to five entries. - SPEC: keep the RBAC role wording with next's shifted section numbers and align the DEREGISTER_AGG_FAUCET section to the FAUCET_ADMIN role terminology. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Post-merge review nits: the config/deregister note-script headers, the register_faucet / store_faucet_metadata_hash panic docs, and the deregister note builder still referred to the 'bridge admin'; update them to the FAUCET_ADMIN role that authority::assert_authorized now enforces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Current state looks fine for me, but I would wait for the #3215 to be merged first, then I'll update this PR and only then re-request the reviews. |
Reconciles the AggLayer bridge RBAC work with #3215, which made role administration fully role-based and removed the Ownable2Step super-admin over the role graph. - Add RoleBasedAccessControl::with_roles(admins, roles) to miden-standards: seeds the built-in ADMIN role plus arbitrary operator roles at genesis (empty member sets are dropped). This lets the bridge stay born-functional while #3215's AccessControl::Rbac convenience enum only seeds ADMIN. - Rework the bridge onto the ADMIN-based stack (RoleBasedAccessControl + Authority, no Ownable2Step): create_bridge_account now takes (seed, admin, BridgeRoles); admin seeds ADMIN, which administers the operational FAUCET_ADMIN / GER_INJECTOR / GER_REMOVER roles. - Update build.rs code-commitment composition, tests/bench, SPEC, and CHANGELOG for the new signature and administration model.
Addresses two review comments:
1. Rename the non-admin faucet role FAUCET_ADMIN -> FAUCET_MNGR (symbol) /
faucet_manager (Rust identifiers). 'Admin' was misleading for an
operational role administered by ADMIN; an admin variation would have
read as FAUCET_ADMIN_ADMIN. Also renames the outdated bridge_admin test
wallets/comments to faucet_manager (they hold the faucet role, not ADMIN).
2. Disambiguate the two RBAC maps that were both spelled 'roles':
- role-members map: RoleBasedAccessControl::with_roles -> with_role_members
(field initial_roles -> initial_role_members).
- procedure-roles map: the 'roles' field of Authority::RbacControlled and
AccessControl::Rbac -> procedure_roles.
|
One thing worth mentioning: I renamed Currently we create our role names internally using pub(crate) struct ShortCapitalString<const MAX_LEN: u8>(String); |
mmagician
left a comment
There was a problem hiding this comment.
LGTM, the remaining comments are all minor and should be trivial to resolve - good to merge after that ✅
|
@Fumuran - there are now some small merge conflicts - could you resolve them when you get a chance? |
…er-rbac-roles # Conflicts: # crates/miden-agglayer/asm/agglayer/bridge/bridge_config.masm # crates/miden-testing/tests/scripts/rbac.rs
Summary
Refactor phase of #2706. Replaces the AggLayer bridge's hard-coded admin / GER-injector / GER-remover account-ID storage slots with the
miden-standardsaccess-control stack (Ownable2Step+RoleBasedAccessControl+Authority).Per the discussion on the issue, this uses RBAC rather than
Ownable2Stepalone, with a separate governance owner over three subordinate operational roles. Each role-gated bridge procedure now callsauthority::assert_authorized, which checks the note sender holds the role mapped to that procedure.Roles
Ownable2Step): top-level authority; grants/revokes roles, two-step transferable.FAUCET_ADMIN→register_faucet,store_faucet_metadata_hashGER_INJECTOR→update_gerGER_REMOVER→remove_gerChanges
RoleAssignment+RoleBasedAccessControl::with_roles(...)to seed initial role members at construction;AccessControl::Rbacgains amembersfield (yields a seeded RBAC instead ofempty()).assert_sender_is_*procs; gate the four privileged procedures viaauthority::assert_authorized.AggLayerBridgeis now stateless; addBridgeRoleMember, role-symbol + procedure-root accessors, and the fixed procedure→role map.create_bridge_accountnow takes(seed, owner, Vec<BridgeRoleMember>).BRIDGE_CODE_COMMITMENT.SPEC.md, agglayer integration tests, and bench setups. Add awith_roles-equals-runtime-grant_roleequivalence test and a procedure→role map pinning test.Out of scope (follow-up)
On-chain role-management notes (
grant_role/revoke_role/transfer_ownership/accept_ownership) plus their allowlist entries and Rust builders. Roles are seeded and enforced, but rotation is not yet exercisable on-chain — hence this is the refactor phase (Refs #2706, notCloses).Testing
cargo +nightly fmt --all --checkandcargo clippy(affected crates,--all-targets): clean.🤖 Generated with Claude Code